/****************************************************************************/
/*  File:       XProcPipeline.java                                          */
/*  Author:     F. Georges - H2O Consulting                                 */
/*  Date:       2010-09-05                                                  */
/*  Tags:                                                                   */
/*      Copyright (c) 2010 Florent Georges (see end of file.)               */
/* ------------------------------------------------------------------------ */


package org.expath.servlex.components;

import com.xmlcalabash.core.XProcException;
import com.xmlcalabash.core.XProcRuntime;
import com.xmlcalabash.io.ReadablePipe;
import com.xmlcalabash.runtime.XPipeline;
import java.util.ArrayList;
import java.util.List;
import javax.xml.transform.SourceLocator;
import net.sf.saxon.s9api.Axis;
import net.sf.saxon.s9api.Processor;
import net.sf.saxon.s9api.QName;
import net.sf.saxon.s9api.SaxonApiException;
import net.sf.saxon.s9api.XdmEmptySequence;
import net.sf.saxon.s9api.XdmItem;
import net.sf.saxon.s9api.XdmNode;
import net.sf.saxon.s9api.XdmNodeKind;
import net.sf.saxon.s9api.XdmSequenceIterator;
import net.sf.saxon.s9api.XdmValue;
import org.apache.log4j.Logger;
import org.expath.pkg.repo.resolver.PkgURIResolver;
import org.expath.servlex.ServlexConstants;
import org.expath.servlex.ServlexException;
import org.expath.servlex.connectors.Connector;
import org.expath.servlex.connectors.XdmConnector;
import org.expath.servlex.runtime.ComponentError;
import org.expath.servlex.tools.CalabashHelper;
import org.expath.servlex.tools.SaxonHelper;

/**
 * ...
 *
 * TODO: Define more precisely the response format.  It is not always easy to
 * generate a sequence of documents in XProc.  Sometimes it is way more convenient
 * to generate a single document wrapping the sequence (e.g. when the documents
 * are generated by a stylesheet).  We should then allow the following:
 *
 * <pre>
 *     &lt;web:wrapper>
 *        &lt;web:response>
 *           ...
 *        &lt;/web:response>
 *        ...
 *        The documents...
 *        ...
 *     &lt;/web:wrapper>
 * </pre>
 *
 * In addition, we have to define how to represent a binary or text content (in
 * XProc that's straightforward: use c:data).  It has to be clearly specified
 * and implemented here...
 *
 * @author Florent Georges
 * @date   2009-12-12
 */
public class XProcPipeline
        implements Component
{
    public XProcPipeline(String pipe)
    {
        myPipe = pipe;
    }

    /**
     * ...
     */
    @Override
    public Connector run(Processor saxon, XProcRuntime calabash, Connector connector)
            throws ServlexException
                 , ComponentError
    {
        try {
            XPipeline pipeline = getPipeline(calabash);
            return evaluatePipeline(saxon, calabash, pipeline, connector);
        }
        catch ( SaxonApiException ex ) {
            LOG.error("User error in pipeline", ex);
            throw SaxonHelper.makeError(ex);
        }
        catch ( XProcException ex ) {
            SourceLocator loc = ex.getLocator();
            LOG.error("User error in pipeline at " + loc.getSystemId() + ":" + loc.getLineNumber(), ex);
            throw CalabashHelper.makeError(ex);
        }
    }

    /**
     * TODO: XPipeline is not cacheable (this is the runtime object).  It is
     * not clear to me what I can use to cache the compiled version (nor whether
     * it is possible at all).  See the email I've sent to XProc-Dev at
     * http://xproc.markmail.org/thread/dhftopkqt6peofcm.
     *
     * The answer is that XPipeline is both the compiled form and the dynamic
     * evaluation representation.  It can be reset thought, but not used
     * concurrently.  So either we compile it every time, or we create some
     * pools of pipeline.  Honestly, I don't think it is worth spending resource
     * on that now.  Most likely the next version of Calabash (well, the v2)
     * will have a much cleaner distinction between compile- and evaluation-time
     * objects (see the above-mentioned thread on XProc-Dev).
     */
    private XPipeline getPipeline(XProcRuntime proc)
            throws SaxonApiException
    {
        LOG.debug("About to compile the pipeline: " + myPipe);
        return proc.load(myPipe);
    }

    /**
     * ...
     * 
     * TODO: Will probably need a specific type of connector, like XProcConnector,
     * to be able to connect to another pipeline based on port names.  For now,
     * this supports only the case where it is called directly from Servlex (no
     * filter nor error handler in between, at least no pipelines, so no other
     * needs than passing through an XDM sequence).
     */
    static Connector evaluatePipeline(Processor saxon, XProcRuntime calabash, XPipeline pipeline, Connector connector)
            throws SaxonApiException
                 , ServlexException
    {
        connector.connectToPipeline(pipeline, saxon, calabash);
        if ( LOG.isDebugEnabled() ) {
            LOG.debug("Existing output ports: " + pipeline.getOutputs());
            for ( String o : pipeline.getOutputs() ) {
                LOG.debug("Existing output port: " + o);
            }
            LOG.debug("The pipeline: " + calabash);
            LOG.debug("The Calabash processor: " + calabash.getProcessor());
            LOG.debug("The Calabash config: " + calabash.getProcessor().getUnderlyingConfiguration());
            LOG.debug("The URI resolver: " + calabash.getProcessor().getUnderlyingConfiguration().getURIResolver());
            LOG.debug("The source resolver: " + calabash.getProcessor().getUnderlyingConfiguration().getSourceResolver());
        }
        // check before running
        if ( ! pipeline.getOutputs().contains(OUTPUT_PORT_NAME) ) {
            throw new ServlexException(501, "The output port '" + OUTPUT_PORT_NAME + "' is mandatory on an XProc pipeline.");
        }
        pipeline.run();
        ReadablePipe response_port = pipeline.readFrom(OUTPUT_PORT_NAME);
        XdmValue result = decodeResponse(response_port);
        return new XdmConnector(result);
    }

    /**
     * Create an XdmValue object from the 'response' port.
     *
     * <pre>
     * if sequence                   # sequence? then use it directly
     *   for item
     *     add item to result
     * else if wrapper               # web:wrapper? unwrap the sequence
     *   for unwrapped
     *     add item to result
     * else                          # a single doc, must be web:response
     *   add doc to result
     * </pre>
     */
    private static XdmValue decodeResponse(ReadablePipe port)
            throws SaxonApiException
                 , ServlexException
    {
        List<XdmItem> result = new ArrayList<XdmItem>();
        port.canReadSequence(true);
        // if there are more than 1 docs, the first one must be web:response,
        // and the following ones are the bodies
        int count = port.documentCount();
        if ( count == 0 ) {
            LOG.debug("The pipeline returned no document on '" + OUTPUT_PORT_NAME + "'.");
            // TODO: If there is no document on the port, we return an empty
            // sequence.  We should probably throw an error instead...
            return XdmEmptySequence.getInstance();
        }
        else if ( count > 1 ) {
            LOG.debug("The pipeline returned " + count + " documents on '" + OUTPUT_PORT_NAME + "'.");
            while ( port.moreDocuments() ) {
                XdmNode doc = port.read();
                addToList(result, doc);
            }
        }
        else {
            LOG.debug("The pipeline returned 1 document on '" + OUTPUT_PORT_NAME + "'.");
            XdmNode response = port.read();
            if ( LOG.isDebugEnabled() ) {
                LOG.debug("Content of the outpot port '" + OUTPUT_PORT_NAME + "': " + response);
            }
            if ( response == null ) {
                // TODO: If there is no web:response, we return an empty sequence.
                // We should probably throw an error instead...
                return XdmEmptySequence.getInstance();
            }
            XdmNode wrapper_elem = getWrapperElem(response);
            // not a web:wrapper, so only one doc, so must be web:response
            if ( wrapper_elem == null ) {
                addToList(result, response);
            }
            // a web:wrapper, so unwrap the sequence
            else {
                XdmSequenceIterator it = wrapper_elem.axisIterator(Axis.CHILD);
                while ( it.hasNext() ) {
                    // TODO: FIXME: For now, due to some strange behaviour in
                    // Calabash, we ignore everything but elements (because it
                    // exposes the indentation as text nodes, which is wrong...)
                    XdmItem child = it.next();
                    if ( child instanceof XdmNode && ((XdmNode) child).getNodeKind() == XdmNodeKind.ELEMENT ) {
                        addToList(result, (XdmNode) child);
                    }
                }
            }
        }
        return new XdmValue(result);
    }

    private static void addToList(List<XdmItem> list, XdmNode node)
            throws ServlexException
    {
        if ( LOG.isDebugEnabled() ) {
            // a document node
            if ( node.getNodeKind() == XdmNodeKind.DOCUMENT ) {
                XdmNode child = getDocElement(node);
                // without element children
                if ( child == null ) {
                    LOG.debug("Adding a document node without any element to the list");
                }
                // with an element child
                else {
                    LOG.debug("Adding a document node with child '" + child.getNodeName() + "' to the list");
                }
            }
            // any other kind of node
            else {
                LOG.debug("Adding the node '" + node.getNodeName() + "' of kind " + node.getNodeKind() + " to the list");
            }
        }
        list.add(node);
    }

    /**
     * Return the root element of {@code doc} (which must be a document node).
     * 
     * Error if there is other element children.
     */
    private static XdmNode getDocElement(XdmNode doc)
            throws ServlexException
    {
        XdmSequenceIterator it = doc.axisIterator(Axis.CHILD);
        XdmNode child = null;
        while ( ( child == null || child.getNodeKind() != XdmNodeKind.ELEMENT ) && it.hasNext() ) {
            child = (XdmNode) it.next();
        }
        while ( it.hasNext() ) {
            XdmNode n = (XdmNode) it.next();
            if ( n.getNodeKind() == XdmNodeKind.ELEMENT ) {
                throw new ServlexException(500, "More than 1 element in a document");
            }
        }
        return child;
    }

    private static XdmNode getWrapperElem(XdmNode node)
            throws SaxonApiException
                 , ServlexException
    {
        if ( node.getNodeKind() == XdmNodeKind.DOCUMENT ) {
            node = getDocElement(node);
            if ( node == null ) {
                return null;
            }
        }
        QName name = node.getNodeName();
        // element(web:wrapper)
        if ( node.getNodeKind() == XdmNodeKind.ELEMENT && name.equals(WRAPPER_NAME) ) {
            LOG.debug("The pipeline returned a web:wrapper");
            return node;
        }
        // is not a wrapper at all
        LOG.debug("The pipeline did not return a web:wrapper (" + name + ")");
        return null;
    }

    /** The name of the input port. */
    public static final String INPUT_PORT_NAME  = "source";
    /** The name of the error port, for error handlers. */
    public static final String ERROR_PORT_NAME  = "user-data";
    /** The name of the output port. */
    public static final String OUTPUT_PORT_NAME = "result";

    /** The logger. */
    private static final Logger LOG = Logger.getLogger(XProcPipeline.class);
    /** QName for 'web:response'. */
    private static final QName WRAPPER_NAME
            = new QName(ServlexConstants.WEBAPP_PREFIX, ServlexConstants.WEBAPP_NS, "wrapper");

    private String myPipe;
}


/* ------------------------------------------------------------------------ */
/*  DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS COMMENT.               */
/*                                                                          */
/*  The contents of this file are subject to the Mozilla Public License     */
/*  Version 1.0 (the "License"); you may not use this file except in        */
/*  compliance with the License. You may obtain a copy of the License at    */
/*  http://www.mozilla.org/MPL/.                                            */
/*                                                                          */
/*  Software distributed under the License is distributed on an "AS IS"     */
/*  basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See    */
/*  the License for the specific language governing rights and limitations  */
/*  under the License.                                                      */
/*                                                                          */
/*  The Original Code is: all this file.                                    */
/*                                                                          */
/*  The Initial Developer of the Original Code is Florent Georges.          */
/*                                                                          */
/*  Contributor(s): none.                                                   */
/* ------------------------------------------------------------------------ */
